import sys, inspect, itertools, types

class CLIException(Exception):
    pass
class CLIInvalidUsageException(CLIException):
    pass
class CLIUnparsableException(CLIException):
    pass
class CLICommandNotFoundException(CLIException):
    pass

_TYPENAMES = {
    None: "Unknown type",
    int:"integer",
    long:"integer",
    float:"floating-point",
    str:"string",
    bool:"yes/no"}
              
                  

class Arguments(object):
    def __init__(self, argv = None):
        super(Arguments, self).__init__()
        if argv is None:
            argv = sys.argv
        self.argv = argv[:]
        self.index = 0
    def __iter__(self):
        return self    
    def next(self):
        if self.index >= len(self.argv):
            raise StopIteration()
        self.index += 1
        return self.argv[self.index - 1]
    def unget(self):
        if self.index <= 0:
            self.index = 0
        else:
            self.index -= 1

class _NONEXISTENT:
    pass

class FunctionArgument(object):
    def __init__(self, argument, default, doc, argtype=None):
        super(FunctionArgument, self).__init__()
        self.name = argument
        self.doc = doc
        self.default = default
        self.type = argtype
        if argtype is None and default is not _NONEXISTENT:
            self.type = type(default)

    def __str__(self):
        if self.default is _NONEXISTENT:
            default="..."
        else:
            default=str(self.default)
        return "%s=%s" % (self.name, default)

class Function(object):
    def __init__(self, obj, name):
        super(Function, self).__init__()
        self.name = name
        func = getattr(obj, name, None)
        if func is None:
            raise CLICommandNotFoundException(name)
        try:
            argnames, argsname, kwargsname, defaults = inspect.getargspec(func)
        except Exception,e:
            raise CLIException("Function %s cannot be inspected" % (name,))
        if type(func) is types.MethodType:
            argnames = argnames[1:]
            func = func.im_func
        argdoc   = getattr(func, "_clippy__argdoc", {})
        argtypes = getattr(func, "_clippy__argtypes", {})
        doc      = getattr(func, "_clippy__doc", None)
        
        if defaults is None:
            defaults = ()
            
        self.args = []
        
        for arg, default in zip(reversed(argnames),
                                         itertools.chain(reversed(defaults),
                                                         itertools.repeat(_NONEXISTENT))):
            self.args.insert(0,
                             FunctionArgument(arg,
                                              default,
                                              argdoc.get(arg, ""),
                                              argtypes.get(arg, None)))
        self.obligatoryArgs = set(arg.name for arg in self.args if arg.default is _NONEXISTENT)
        self.collectsArguments= (argsname is not None)
        self.collectsKwArguments = (kwargsname is not None)
        self.doc = doc or func.__doc__ or ""
        
    @property
    def usage(self):
        return "%s %s\n%s" % (self.name,
                              " ".join(map(str, self.args)),
                              self.doc)
    @property
    def fullUsage(self):
        returned = "%s\n\t%s\n\tArguments:" % (self.name, self.doc)
        for arg in self.args:
            returned += "\n\t%s (%s): %s%s" % (arg.name,
                                               _TYPENAMES.get(arg.type, None) or arg.type.__name__,
                                               arg.doc,
                                               arg.default is not _NONEXISTENT and "(default=%s)" % arg.default or "")
        return returned
        


def Functions(obj):
    for command in sorted(dir(obj)):
        if command.startswith("_"):
            continue
        try:
            func = Function(obj, command)
        except CLICommandNotFoundException:
            continue
        yield func
        
class Parser(object):
    """The clippy parser"""
    def __init__(self,
                 helpCommand = "help",
                 deduceTypesFromDefaults=True):
        super(Parser, self).__init__()
        self.helpCommand = helpCommand
        self.deduceTypesFromDefaults = deduceTypesFromDefaults

    def displayHelp(self, obj):
        for func in Functions(obj):
            print func.fullUsage
    
    def _assertObjectIsSuitable(self, obj, numCommands):
        for funcname in dir(obj):
            if funcname.startswith("_"):
                continue
            func = getattr(obj, funcname)
            if not callable(func):
                continue
                    
        
    def run(self, obj, args = None, numCommands = None):
        if args is None:
            args = sys.argv[1:]
        self._assertObjectIsSuitable(obj, numCommands)
            
        args = Arguments(args)
        for index, command in enumerate(args):
            if numCommands is not None and index == numCommands:
                return
            if command == self.helpCommand:
                self.displayHelp(obj)
                continue
            func = Function(obj, command)
            
            kwargs = dict()
            func_args = func.args[:]
        
            while True:
                if not func_args:
                    break
                try:
                    argstr = args.next()
                except StopIteration:
                    break
                if "=" in argstr:
                    argname, argvalue = argstr.split("=")
                    matching = [x for x in func_args if x.name == argname]
                    if not matching and not func.collectsKwArguments:
                        raise CLIInvalidUsageException("Command %s "
                                     "does not receive an argument "
                                     "named %s" % (command, argname))
                    assert len(matching) == 1
                    argobj = matching[0]
                    func_args.remove(argobj)
                else:
                    # this is a command!!!
                    args.unget()
                    break
                if argname in kwargs:
                    raise CLIUnparsableException("Multiple values for argument %s of command %s specified!" % (argname, command))
                kwargs[argname] = argvalue
            unsatisfied = set(arg.name for arg in func.args
                              if arg.default is  _NONEXISTENT) - set(kwargs)
            if unsatisfied:
                raise CLIInvalidUsageException("Command %s expects additional arguments (%s)" % (command, ", ".join(sorted(unsatisfied))))
            getattr(obj, command)(**kwargs)
                
##### USEFUL DECORATORS #####
def argdoc(**kwargs):
    def decorator(func):
        docs = getattr(func, "_clippy__argdoc", {})
        docs.update(kwargs)
        func._clippy__argdoc = docs
        return func
    return decorator

def argtypes(**kwargs):
    def decorator(func):
        types = getattr(func, "_clippy__argtypes", {})
        types.update(kwargs)
        func._clippy__argtypes = types
        return func
    return decorator
            
def doc(documentation):
    def decorator(func):
        func._clippy__doc = documentation
        return func
    return decorator

    
